/*
 * @(#)KeepAliveCache.java	1.28 06/10/10
 *
 * Copyright  1990-2008 Sun Microsystems, Inc. All Rights Reserved.  
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER  
 *   
 * This program is free software; you can redistribute it and/or  
 * modify it under the terms of the GNU General Public License version  
 * 2 only, as published by the Free Software Foundation.   
 *   
 * This program is distributed in the hope that it will be useful, but  
 * WITHOUT ANY WARRANTY; without even the implied warranty of  
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU  
 * General Public License version 2 for more details (a copy is  
 * included at /legal/license.txt).   
 *   
 * You should have received a copy of the GNU General Public License  
 * version 2 along with this work; if not, write to the Free Software  
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  
 * 02110-1301 USA   
 *   
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa  
 * Clara, CA 95054 or visit www.sun.com if you need additional  
 * information or have any questions. 
 *
 */

package sun.net.www.http;

import java.io.InputStream;
import java.io.IOException;
import java.io.NotSerializableException;
import java.util.*;
import java.net.URL;

/**
 * A class that implements a cache of idle Http connections for keep-alive
 *
 * @version 1.28 10/10/06 
 * @author Stephen R. Pietrowicz (NCSA)
 * @author Dave Brown
 */
public class KeepAliveCache extends Hashtable implements Runnable {
    private static final long serialVersionUID = -2937172892064557949L;

    /* maximum # keep-alive connections to maintain at once
     * This should be 2 by the HTTP spec, but because we don't support pipe-lining
     * a larger value is more appropriate. So we now set a default of 5, and the value
     * refers to the number of idle connections per destination (in the cache) only.
     * It can be reset by setting system property "http.maxConnections".
     */
    static final int MAX_CONNECTIONS = 5;
    static int result = -1;
    static int getMaxConnections() {
        if (result == -1) {
	    result = ((Integer)java.security.AccessController.doPrivileged(
	        new sun.security.action.GetIntegerAction("http.maxConnections",
							 MAX_CONNECTIONS))).intValue();
	    if (result <= 0)
		result = MAX_CONNECTIONS;
	}
	    return result;
    }

    static final int LIFETIME = 5000;

    private Thread keepAliveTimer = null;

    /**
     * Constructor
     */
    public KeepAliveCache() {}

    /**
     * Register this URL and HttpClient (that supports keep-alive) with the cache
     * @param url  The URL contains info about the host and port
     * @param http The HttpClient to be cached
     */
    public synchronized void put(final URL url, Object obj, HttpClient http) {
	boolean startThread = (keepAliveTimer == null);
	if (!startThread) {
	    if (!keepAliveTimer.isAlive()) {
		startThread = true;
	    }
	}
	if (startThread) {
	    clear();
	    /* Unfortunately, we can't always believe the keep-alive timeout we got
	     * back from the server.  If I'm connected through a Netscape proxy
	     * to a server that sent me a keep-alive
	     * time of 15 sec, the proxy unilaterally terminates my connection
	     * The robustness to to get around this is in HttpClient.parseHTTP()
	     */
	    final KeepAliveCache cache = this;
	    java.security.AccessController.doPrivileged(
		new java.security.PrivilegedAction() {
		public Object run() {
                   // We want to create the Keep-Alive-Timer in the
		    // system threadgroup
		    ThreadGroup grp = Thread.currentThread().getThreadGroup();
		    ThreadGroup parent = null;
		    while ((parent = grp.getParent()) != null) {
			grp = parent;
		    }

		    keepAliveTimer = new Thread(grp, cache, "Keep-Alive-Timer");
		    keepAliveTimer.setDaemon(true);
		    keepAliveTimer.setPriority(Thread.MAX_PRIORITY - 2);
		    keepAliveTimer.start();
		    return null;
		}
	    });
	}

	KeepAliveKey key = new KeepAliveKey(url, obj);
	ClientVector v = (ClientVector)super.get(key);

	if (v == null) {
	    int keepAliveTimeout = http.getKeepAliveTimeout();
	    v = new ClientVector(keepAliveTimeout > 0?
				 keepAliveTimeout*1000 : LIFETIME);
	    v.put(http);
	    super.put(key, v);
	} else {
	    v.put(http);
	}
    }

    /* remove an obsolete HttpClient from it's VectorCache */
    public synchronized void remove (HttpClient h, Object obj) {
 	KeepAliveKey key = new KeepAliveKey(h.url, obj);
  	ClientVector v = (ClientVector)super.get(key);
 	if (v != null) {
 	    v.remove(h);
 	    if (v.empty()) {
 		removeVector(key);
 	    }
  	}
    }

    /* called by a clientVector thread when all it's connections have timed out
     * and that vector of connections should be removed.
     */
    synchronized void removeVector(KeepAliveKey k) {
	super.remove(k);
    }

    /**
     * Check to see if this URL has a cached HttpClient
     */
    public synchronized Object get(URL url, Object obj) {

	KeepAliveKey key = new KeepAliveKey(url, obj);
	ClientVector v = (ClientVector)super.get(key);
	if (v == null) { // nothing in cache yet
	    return null;
	}
	return v.get();
    }

    /* Sleeps for an alloted timeout, then checks for timed out connections.
     * Errs on the side of caution (leave connections idle for a relatively
     * short time).
     */
    public void run() {
	int total_cache;
	do {
	    try {
		Thread.sleep(LIFETIME);
	    } catch (InterruptedException e) {}
	    synchronized (this) {
		/* Remove all unused HttpClients.  Starting from the
		 * bottom of the stack (the least-recently used first).
		 * TODO: It'd be nice to not remove *all* connections
		 * that aren't presently in use.  One could have been added
		 * a second ago that's still perfectly valid, and we're
		 * needlessly axing it.  But it's not clear how to do this
		 * cleanly, and doing it right may be more trouble than it's
		 * worth.
		 */
		
		long currentTime = System.currentTimeMillis(); 

		Iterator itr = keySet().iterator();
		ArrayList keysToRemove = new ArrayList();
		
		while (itr.hasNext()) {
		    KeepAliveKey key = (KeepAliveKey)itr.next();
		    ClientVector v = (ClientVector)get(key);
		    synchronized (v) {
			int i; 

			for (i = 0; i < v.size(); i++) { 
			    KeepAliveEntry e = (KeepAliveEntry)v.elementAt(i);
			    if ((currentTime - e.idleStartTime) > v.nap) { 
				HttpClient h = e.hc; 
				h.closeServer();
			    } else {
				break;
			    }
			} 
			v.subList(0, i).clear(); 
			
			if (v.size() == 0) { 
			    keysToRemove.add(key);
			}
		    }
		}
		itr = keysToRemove.iterator();
		while (itr.hasNext()) {
		    removeVector((KeepAliveKey)itr.next());
		}
	    }
	} while (size() > 0); 
	
	return;
    }

    /*
     * Do not serialize this class!
     */
    private void writeObject(java.io.ObjectOutputStream stream)
    throws IOException {
	throw new NotSerializableException();
    }

    private void readObject(java.io.ObjectInputStream stream)
    throws IOException, ClassNotFoundException {
	throw new NotSerializableException();
    }
}

/* FILO order for recycling HttpClients, should run in a thread
 * to time them out.  If > maxConns are in use, block.
 */


class ClientVector extends java.util.Stack {
    private static final long serialVersionUID = -8680532108106489459L;

    // sleep time in milliseconds, before cache clear
    int nap;

    

    ClientVector (int nap) {
	this.nap = nap;
    }

    synchronized HttpClient get() {
	if (empty()) {
	    return null;
	} else {
	    // Loop until we find a connection that has not timed out
	    HttpClient hc = null;
	    long currentTime = System.currentTimeMillis();
	    do {
		KeepAliveEntry e = (KeepAliveEntry)pop();
		if ((currentTime - e.idleStartTime) > nap) {
		    e.hc.closeServer();
		} else {
		    hc = e.hc;
		}
	    } while ((hc== null) && (!empty()));
	    return hc;
	}
    }

    /* return a still valid, unused HttpClient */
    synchronized void put(HttpClient h) {
	if (size() > KeepAliveCache.getMaxConnections()) { 
	    h.closeServer(); // otherwise the connection remains in limbo
	} else {
	    push(new KeepAliveEntry(h, System.currentTimeMillis()));
	}
    }

    /*
     * Do not serialize this class!
     */
    private void writeObject(java.io.ObjectOutputStream stream)
    throws IOException {
	throw new NotSerializableException();
    }

    private void readObject(java.io.ObjectInputStream stream)
    throws IOException, ClassNotFoundException {
	throw new NotSerializableException();
    }
}


class KeepAliveKey {
    private String	protocol = null;
    private String	host = null;
    private int		port = 0;
    private Object      obj = null; // additional key, such as socketfactory

    /**
     * Constructor
     *
     * @param url the URL containing the protocol, host and port information
     */
    public KeepAliveKey(URL url, Object obj) {
	this.protocol = url.getProtocol();
	this.host = url.getHost();
	this.port = url.getPort();
	this.obj = obj;
    }

    /**
     * Determine whether or not two objects of this type are equal
     */
    public boolean equals(Object obj) {
	if ((obj instanceof KeepAliveKey) == false)
	    return false;
	KeepAliveKey kae = (KeepAliveKey)obj;
	return host.equals(kae.host)
	    && (port == kae.port)
	    && protocol.equals(kae.protocol)
	    && this.obj == kae.obj;
    }

    /**
     * The hashCode() for this object is the string hashCode() of
     * concatenation of the protocol, host name and port.
     */
    public int hashCode() {
	String str = protocol+host+port;
	return this.obj == null? str.hashCode() :
	    str.hashCode() + this.obj.hashCode();
    }
}

class KeepAliveEntry {
    HttpClient hc;
    long idleStartTime;
    
    KeepAliveEntry(HttpClient hc, long idleStartTime) {
	this.hc = hc;
	this.idleStartTime = idleStartTime;
    }
}
